package com.gao.service.impl;

import com.gao.config.MinioConfig;
import com.gao.service.FileService;
import com.gao.vo.Base64Vo;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import jdk.internal.util.xml.impl.Input;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * @author gao
 * @date 2023/05/12 23:21:39
 */
@Slf4j
@Service
public class MinoFileServiceImpl implements FileService {

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinioConfig minioConfig;

    @Override
    public String uploadFile(MultipartFile file) {
        String newFileName = generateNewFileName(file.getOriginalFilename());
        return doUploadFile(file, newFileName);
    }

    @Override
    public String uploadBase64File(Base64Vo base64Vo) {
        String base64Str = base64Vo.getBase64();
        File file = base64ToFile(base64Vo, base64Str);
        String newFileName = generateNewFileName(file.getName());
        CommonsMultipartFile multipartFile = transferFileToMultipartFile(base64Vo, file);
        return doUploadFile(multipartFile, newFileName);
    }

    @Override
    public String uploadSingleFileWithHashFilename(MultipartFile file, String filename) {
        if (isFileExists(filename)) {
            // 如果文件已经存在，则直接返回该文件的url
            return getUrl(filename);
        }
        return doUploadFile(file, filename);
    }

    @NotNull
    private String getUrl(String filename) {
        return minioConfig.getUrl() + "/" + minioConfig.getBucketName() + "/" + filename;
    }

    private boolean isFileExists(String filename) {
        StatObjectArgs statObjectArgs = StatObjectArgs.builder().bucket(minioConfig.getBucketName()).object(filename).build();
        try {
            StatObjectResponse statObjectResponse = minioClient.statObject(statObjectArgs);
            return statObjectResponse != null;
        } catch (Exception e) {
            return false;
        }
    }


    private CommonsMultipartFile transferFileToMultipartFile(Base64Vo base64Vo, File file) {
        FileItem item = new DiskFileItem("file"
                , base64Vo.getType()
                , true
                , file.getName()
                , (int) file.length()
                , file.getParentFile());
        try (
                OutputStream out = item.getOutputStream();
                InputStream in = new FileInputStream(file);
        ) {
            byte[] buffer = new byte[8192];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return new CommonsMultipartFile(item);
    }

    private File base64ToFile(Base64Vo base64Vo, String base64Str) {
        try {
            File tmpFile = File.createTempFile("foo", getFileSuffix(base64Vo.getName()));
            try (OutputStream out = new FileOutputStream(tmpFile)) {
                byte[] buffer = Base64.decodeBase64(base64Str);
                out.write(buffer);
            }
            return tmpFile;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private String doUploadFile(MultipartFile file, String newFileName) {
        try (InputStream inputStream = file.getInputStream()) {
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(newFileName)
                    .stream(inputStream, file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build();
            minioClient.putObject(putObjectArgs);
            return getUrl(newFileName);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private String generateNewFileName(String originalFilename) {
        String suffix = getFileSuffix(originalFilename);
        String newFileName = UUID.randomUUID().toString() + suffix;
        return newFileName;
    }

    private String getFileSuffix(String originalFilename) {
        int lastDotIndex = originalFilename.lastIndexOf('.');
        String suffix = originalFilename.substring(lastDotIndex);
        return suffix;
    }

    @Override
    public String uploadBigFile(MultipartFile file, String hash, int pieceIndex, int totalPieces, String suffix) {
        log.info("大文件hash: {}", hash);
        int nextIndexToUpload = uploadFilePiece(file, hash, pieceIndex, totalPieces);
        if (nextIndexToUpload == FINISH_UPLOAD) {
            log.info("所有分片上传完成,开始合并分片");
            String bigFileHash = mergeFile(hash, totalPieces, suffix);
            deleteFilePieces(hash, totalPieces);
            if (!hash.equals(bigFileHash)) {
                throw new RuntimeException("大文件合并失败：大文件hash与客户端传递的hash不一致");
            }
            String url = minioConfig.getUrl().concat("/").concat(minioConfig.getBucketName()).concat(hash).concat(".").concat(suffix);
            return url;
        }
        return String.valueOf(nextIndexToUpload);
    }

    private static final int FINISH_UPLOAD = -1;
    private static final String TEMP_DIR = "temp";
    private static final String BIG_FILE_DIR = "bigfile";

    /**
     * 上传单独的文件分片
     *
     * @return 返回需要上传的文件序号，-1表示已经上传完成
     */
    private int uploadFilePiece(MultipartFile file, String hash, int pieceIndex, int totalPieces) {

        makeUploadTempDir(TEMP_DIR);

        // 如果当前分片已经上传过，就直接跳过（端点续传）
        if (isFilePieceExists(hash, pieceIndex)) {
            log.info("{}分片已存在", pieceIndex);
            if (pieceIndex < totalPieces - 1) {
                return ++pieceIndex;
            }
            // 如果当前已经上传过的分片是最后一个分片，就直接返回上传完毕的信号
            return FINISH_UPLOAD;
        }

        log.info("上传{}分片开始", pieceIndex);
        int nextIndexToUpload = doUploadFile(file, hash, pieceIndex, totalPieces);
        log.info("上传{}分片结束", pieceIndex);
        return nextIndexToUpload;
    }


    private void makeUploadTempDir(String dir) {
        try {
            if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(dir).build())) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(dir).build());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private boolean isFilePieceExists(String hash, int pieceIndex) {
        // 如果指定的object不存在，则getObject会抛出异常
        try (GetObjectResponse object = minioClient.getObject(GetObjectArgs.builder().bucket(TEMP_DIR).object(hash.concat("/").concat(String.valueOf(pieceIndex))).build())) {
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public int getIndexToUpload(String hash, int totalPieces) {

        Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(TEMP_DIR).prefix(hash.concat("/")).build());
        List<String> objectNames = StreamSupport.stream(results.spliterator(), false).map(
                r -> {
                    try {
                        return r.get().objectName();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
        ).collect(Collectors.toList());
        List<Integer> restPieceIndexes = Stream.iterate(0, i -> ++i)
                .limit(totalPieces)
                .filter(i -> !objectNames.contains(hash.concat("/").concat(String.valueOf(i))))
                .sorted()
                .collect(Collectors.toList());
        if (restPieceIndexes.size() > 0) {
            // 获取到下一个该上传的分片的索引
            Integer indexToUpload = restPieceIndexes.get(0);
            return indexToUpload;
        }
        // 返回 FINISH_UPLOAD 则表示，没有需要继续上传的分片了，所有分片上传完毕
        return FINISH_UPLOAD;
    }

    private int doUploadFile(MultipartFile file, String hash, int pieceIndex, int totalPieces) {
        try (InputStream in = file.getInputStream()) {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(TEMP_DIR)
                    .object(hash.concat("/").concat(String.valueOf(pieceIndex)))
                    .stream(in, file.getSize(), -1)
                    .contentType(file.getContentType()).build());
            if (pieceIndex < totalPieces - 1) {
                return ++pieceIndex;
            }
            return FINISH_UPLOAD;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private String mergeFile(String hash, int totalPieces, String suffix) {
        List<ComposeSource> sources = Stream.iterate(0, i -> ++i)
                .limit(totalPieces)
                .map(i -> ComposeSource.builder()
                        .bucket(TEMP_DIR)
                        .object(hash.concat("/").concat(String.valueOf(i)))
                        .build()
                ).collect(Collectors.toList());

        makeUploadTempDir(BIG_FILE_DIR);

        try {
            ObjectWriteResponse response = minioClient.composeObject(ComposeObjectArgs.builder()
                    .bucket(BIG_FILE_DIR)
                    .object(hash.concat(".").concat(suffix))
                    .sources(sources)
                    .build());
            log.info("合并分片完毕");

            try (
                    GetObjectResponse object = minioClient.getObject(GetObjectArgs.builder()
                            .bucket(response.bucket())
                            .object(response.object())
                            .build());
            ) {
                String bigFileHash = DigestUtils.md5Hex(object);
                return bigFileHash;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void deleteFilePieces(String hash, int totalPieces) {
        try {
            List<DeleteObject> delObjects = Stream.iterate(0, i -> ++i)
                    .limit(totalPieces)
                    .map(i -> new DeleteObject(hash.concat("/").concat(String.valueOf(i)))).collect(Collectors.toList());
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder()
                    .bucket(TEMP_DIR)
                    .objects(delObjects)
                    .build());
            for (Result<DeleteError> result : results) {
                DeleteError deleteError = result.get();
                log.error("Error in deleting object " + deleteError.objectName() + "; " + deleteError.message());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


}
